﻿//
// © 2003-2004 Ryan Seghers
// http://www.rseghers.com/codeAndUtils.htm
//
using System;
using System.Diagnostics;
using System.Threading;
using System.Runtime.InteropServices;

namespace Algobox.Toolbox.Diagnostics
{
    public class DateTimePrecise
    {
        #region Dll Import

        /// <summary>
        /// The ticks returned by this function are not the same as
        /// .Net's ticks.
        /// </summary>
        [DllImport("kernel32", EntryPoint = "QueryPerformanceCounter", SetLastError = true)]
        private static extern bool K32QueryPerformanceCounter(out long t);

        /// <summary>
        /// This returns Kernel32 ticks/sec.
        /// </summary>
        [DllImport("kernel32", EntryPoint = "QueryPerformanceFrequency", SetLastError = true)]
        private static extern bool K32QueryPerformanceFrequency(out long t);

        #endregion

        #region Fields

        /// <summary>
        /// Frequency obtained from kernel32.  This is in kernel32's ticks 
        /// per second.
        /// </summary>
        static long perfFreq = 0;

        /// <summary>
        /// The minimum duration that this class is able to measure. 
        /// This is measured during Calibrate().
        /// This represents the number of ticks it takes to make
        /// a duration measurement, and is therefore the effective 
        /// resolution of this class.
        /// </summary>
        static long overheadTicks;

        /// <summary>
        /// This stores the offset between Kernel32's ticks and .Net's
        /// DateTime's ticks, so that we can produce accurate DateTime's.
        /// </summary>
        static long k32TicksOffset;

        #endregion

        #region Properties
             

        /// <summary>
        /// Gets the tick count from the clock.
        /// </summary>
        /// <remarks>No adjustment is made to this due to OverheadTicks.</remarks>
        public static long NowTicks
        {
            get
            {
                if (perfFreq == 0)
                    throw new NotSupportedException("This system does not have a high performance counter.");

                long t = 0;
                if (!K32QueryPerformanceCounter(out t))
                {
                    int errorCode = Marshal.GetLastWin32Error();
                    throw new NotSupportedException(
                        String.Format("This system returned win32 error code {0} when the performance counter was queried.",
                        errorCode));
                }
                // convert to .Net ticks
                long ticks = (long)(t / ((float)perfFreq / 1e7));
                return (ticks);
            }
        }

        /// <summary>
        /// Return a DateTime which represents the current time.  This is similar
        /// to DateTime.Now, but is higher resolution.
        /// <seealso cref="System.DateTime"/>
        /// </summary>
        /// <remarks>No adjustment is made to this due to OverheadTicks.</remarks>
        public static DateTime Now
        {
            get
            {
                return (new DateTime(NowTicks - k32TicksOffset));
            }
        }

        /// <summary>
        /// The minimum duration that this class is able to measure. 
        /// This is measured during Calibrate().
        /// This represents the number of ticks it takes to make
        /// a duration measurement, and is therefore the effective 
        /// resolution of this class.
        /// This is subtracted from all duration calculations, which should 
        /// compensate for the clock read overhead.
        /// </summary>
        public static long OverheadTicks
        {
            get { return (overheadTicks); }
        }

        /// <summary>
        /// This returns the number of high performance counter ticks (NOT .Net ticks) per second,
        /// as measured by Kernel32.dll's QueryPerformanceFrequency() function.
        /// </summary>
        public static long Frequency
        {
            get { return (perfFreq); }
        }

        #endregion

        #region Constructor

        /// <summary>Not supposed to instantiate this class.</summary>
        private DateTimePrecise() { }

        /// <summary>Static constructor.</summary>
        static DateTimePrecise()
        {
            Calibrate();
        }

        #endregion

        #region Main Public API

        /// <summary>
        /// Measure overhead for interop calls and clock resolution.
        /// The resultant values are stored in static fields.
        /// The resultant clock frequency and function call overhead
        /// measurement are used in ticks to ms conversion and 
        /// duration calculations, respectively.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This is called from the static constructor, so you typically
        /// won't benefit from calling it, unless during construction
        /// there were other very high priority things going on that
        /// affected the timing of the statements in this method.
        /// </para>
        /// <para>
        /// This temporarily boosts this process and thread priority
        /// to avoid being affected by other processes/threads, so you 
        /// definitely don't want to call this a lot.
        /// </para>
        /// </remarks>
        public static void Calibrate()
        {
            if (!K32QueryPerformanceFrequency(out perfFreq))
            {
                int errorCode = Marshal.GetLastWin32Error();
                throw new NotSupportedException(
                    String.Format("This system returned win32 error code {0} when the performance frequency was queried.",
                    errorCode));
            }

            // boost process and thread priority temporarily
            ThreadPriority oldThreadPriority = Thread.CurrentThread.Priority;
            Thread.CurrentThread.Priority = ThreadPriority.AboveNormal;

            Process proc = Process.GetCurrentProcess();
            ProcessPriorityClass oldProcPriorityClass = proc.PriorityClass;
            proc.PriorityClass = ProcessPriorityClass.RealTime;

            try
            {
                // calc offset between our ticks and DateTime
                k32TicksOffset = NowTicks - DateTime.Now.Ticks;

                // calculate measurement overhead by taking a set of measurements
                long start, stop = 0;
                overheadTicks = Int64.MaxValue;

                for (int i = 0; i < 10; i++)
                {
                    start = NowTicks;
                    stop = NowTicks;
                    long ticks = stop - start;
                    if (ticks > 0 && ticks < overheadTicks) overheadTicks = ticks;
                }

                // handle case where no measurement gave us a difference
                // in case the overhead is under the perf counter resolution
                if (overheadTicks == Int64.MaxValue) overheadTicks = 0;

                //Console.WriteLine("perfFreq = {0}, overheadTicks = {1}, overheadMs = {2}", 
                //	perfFreq, overheadTicks, TicksToMs(overheadTicks));
            }
            finally
            {
                Thread.CurrentThread.Priority = oldThreadPriority;
                proc.PriorityClass = oldProcPriorityClass;
            }
        }

        /// <summary>
        /// Convert ticks to milliseconds. (A tick is 100 nanoseconds.)
        /// </summary>
        /// <remarks>No adjustment is made to this due to OverheadTicks.</remarks>
        /// <param name="ticks">The tick count.</param>
        /// <returns>float - the number of milliseconds.</returns>
        public static float TicksToMs(long ticks)
        {
            return ((float)ticks / 10000);
        }

        /// <summary>
        /// Calculate the duration as a TimeSpan.
        /// <seealso cref="System.TimeSpan"/>
        /// </summary>
        /// <remarks>This subtracts OverheadTicks from the measured ticks
        /// to compensate for overhead.</remarks>
        /// <param name="startTicks">The starting tick count.</param>
        /// <param name="stopTicks">The stopTicks tick count.</param>
        /// <returns>A new TimeSpan.</returns>
        public static TimeSpan CalcTimeSpan(long startTicks, long stopTicks)
        {
            long ticks = stopTicks - startTicks;
            ticks -= overheadTicks;
            ticks = System.Math.Max(0, ticks);
            return (new TimeSpan(ticks));
        }

        /// <summary>
        /// Calculate the duration (to NowTicks) as a TimeSpan.
        /// <seealso cref="System.TimeSpan"/>
        /// </summary>
        /// <remarks>This subtracts OverheadTicks from the measured ticks
        /// to compensate for overhead.</remarks>
        /// <param name="startTicks">The starting tick count.</param>
        /// <returns>A new TimeSpan.</returns>
        public static TimeSpan CalcTimeSpan(long startTicks)
        {
            return (CalcTimeSpan(startTicks, NowTicks));
        }

        /// <summary>
        /// Calculate the duration (to NowTicks) as a TimeSpan.
        /// <seealso cref="System.TimeSpan"/>
        /// </summary>
        /// <remarks>This subtracts OverheadTicks from the measured ticks
        /// to compensate for overhead.</remarks>
        /// <param name="startTime">The starting DateTime.</param>
        /// <returns>A new TimeSpan.</returns>
        public static TimeSpan CalcTimeSpan(DateTime startTime)
        {
            return (CalcTimeSpan(startTime.Ticks, NowTicks));
        }

        /// <summary>
        /// Calculate the duration (to NowTicks) as a TimeSpan.
        /// <seealso cref="System.TimeSpan"/>
        /// </summary>
        /// <remarks>This subtracts OverheadTicks from the measured ticks
        /// to compensate for overhead.</remarks>
        /// <param name="startTime">The starting DateTime.</param>
        /// <param name="stopTime">The stopping DateTime.</param>
        /// <returns>A new TimeSpan.</returns>
        public static TimeSpan CalcTimeSpan(DateTime startTime, DateTime stopTime)
        {
            return (CalcTimeSpan(startTime.Ticks, stopTime.Ticks));
        }

        #endregion

        #region Test Methods

        /// <summary>
        /// A simple self test.
        /// </summary>
        /// <returns>bool - true for test passed, false for failed</returns>
        public static bool TestSelf()
        {
            //HighResClock.Calibrate();
            Console.WriteLine("Measurement overhead: {0} ticks", DateTimePrecise.OverheadTicks);
            Console.WriteLine("Frequency: {0} cpuTicks/sec", DateTimePrecise.Frequency);

            double realFreq = 1.4 * 1024 * 1024 * 1024;
            Console.WriteLine("1.4 GHz CPU clock freq / perf counter freq = {0:f3}",
                 realFreq / DateTimePrecise.Frequency);

            // figure out how often high perf counter wraps
            double rangeSeconds = (double)long.MaxValue / DateTimePrecise.Frequency;
            double rangeDays = rangeSeconds / (3600 * 24);
            double rangeYears = rangeDays / 365;
            Console.WriteLine("Span of clock is {0:e3} s, {1:e3} d, {2:e3} years",
                rangeSeconds, rangeDays, rangeYears);

            long start = DateTimePrecise.NowTicks;
            long stop = DateTimePrecise.NowTicks;
            long stop1 = DateTimePrecise.NowTicks;

            Console.WriteLine("duration: {0} ms", DateTimePrecise.CalcTimeSpan(start, stop).TotalMilliseconds);
            Console.WriteLine("duration1: {0} ms", DateTimePrecise.CalcTimeSpan(start, stop1).TotalMilliseconds);
            Console.WriteLine("duration: {0} ms", DateTimePrecise.CalcTimeSpan(start).TotalMilliseconds);

            // check against sleep
            start = DateTimePrecise.NowTicks;
            int sleepTime = 100;
            Thread.Sleep(sleepTime);
            stop = DateTimePrecise.NowTicks;
            double d = DateTimePrecise.CalcTimeSpan(start, stop).TotalMilliseconds;
            Console.WriteLine("Slept {0} ms and that gave measured {1:f2} ms",
                sleepTime, d);
            Console.WriteLine("    (This is typically a little off (5ms?), probably due to "
                + "thread scheduling latencies.)");

            // try TimeSpan
            TimeSpan span = DateTimePrecise.CalcTimeSpan(start, stop);
            Console.WriteLine("The same duration as a time span is {0} ms", span.TotalMilliseconds);

            // try DateTime
            Console.WriteLine("My DateTime: {0},   .Net's DateTime: {1}",
                DateTimePrecise.Now, DateTime.Now);

            //
            // time something with DateTime
            //
            DateTime startDateTime = DateTime.Now;
            DateTime startHrc = DateTimePrecise.Now;

            // kill some time
            double res = 0.0;
            for (int i = 0; i < 50000; i++)
            {
                res += System.Math.Sin(System.Math.Cos(System.Math.Acos(1.0)));
            }

            span = DateTime.Now - startDateTime;
            TimeSpan spanHrc = DateTimePrecise.Now - startHrc;
            Console.WriteLine("DateTime's timing: {0} ms", span.TotalMilliseconds);
            Console.WriteLine("HighResClock's timing: {0} ms", spanHrc.TotalMilliseconds);

            //
            // check DateTime's resolution
            //
            startDateTime = DateTime.Now;
            double maxMsGap = 100000;
            for (int i = 0; i < 100; i++)
            {
                double msGap = ((TimeSpan)(DateTime.Now - startDateTime)).TotalMilliseconds;
                Thread.Sleep(1);
                if ((msGap > 0) && (msGap < maxMsGap)) maxMsGap = msGap;
            }
            Console.WriteLine("DateTime's resolution is {0:f2} ms", maxMsGap);

            //
            // check HighResClock's DateTime resolution
            //
            startDateTime = DateTimePrecise.Now;
            maxMsGap = 100000;
            for (int i = 0; i < 100; i++)
            {
                double msGap = ((TimeSpan)(DateTimePrecise.Now - startDateTime)).TotalMilliseconds;
                Thread.Sleep(1);
                if ((msGap > 0) && (msGap < maxMsGap)) maxMsGap = msGap;
            }
            Console.WriteLine("HighResClock's DateTime resolution is {0} ms", maxMsGap);

            return (true);
        }

        #endregion
    }
}
